import { createReadStream, promises as fs } from "node:fs"; import { Readable } from "node:stream"; import { publicFile } from "@/lib/papers/queries"; import { recordEvent } from "@/lib/papers/events"; import { storagePath } from "@/lib/pdf/process"; import { ip, limited, tooMany } from "@/lib/api"; export const dynamic = "force-dynamic"; /** Serves a published manuscript version (content-addressed on disk). Counts one download per visitor per paper per day. Supports Range and HEAD. */ export async function GET(req: Request, ctx: { params: Promise<{ fileId: string; name: string }> }) { return serve(req, ctx, false); } export async function HEAD(req: Request, ctx: { params: Promise<{ fileId: string; name: string }> }) { return serve(req, ctx, true); } async function serve(req: Request, { params }: { params: Promise<{ fileId: string; name: string }> }, head: boolean) { if (limited(req, "files", 60)) return tooMany(); const { fileId, name } = await params; if (!/^[0-9a-f-]{36}$/.test(fileId)) return new Response("Not found", { status: 404 }); const f = await publicFile(fileId); if (!f) return new Response("Not found", { status: 404 }); const path = storagePath(f.storage_key); let st; try { st = await fs.stat(path); } catch { return new Response("Missing file", { status: 404 }); } const inline = new URL(req.url).searchParams.get("inline") === "1"; const filename = name.endsWith(".pdf") ? name.replace(/[^A-Za-z0-9._-]/g, "_") : `${f.slug}-${f.label}.pdf`; const headers: Record = { "Content-Type": "application/pdf", "Content-Disposition": `${inline ? "inline" : "attachment"}; filename="${filename}"`, "Accept-Ranges": "bytes", "Cache-Control": "public, max-age=86400, immutable", ETag: `"${f.sha256}"`, "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "sandbox", "X-Checksum-SHA256": f.sha256 }; if (req.headers.get("if-none-match") === `"${f.sha256}"`) return new Response(null, { status: 304, headers }); if (!head && !inline) recordEvent("download", f.paper_id, { ip: ip(req), ua: req.headers.get("user-agent"), referrer: req.headers.get("referer"), versionId: f.version_id }).catch(() => {}); const range = req.headers.get("range"); let start = 0, end = st.size - 1, status = 200; if (range) { const m = /bytes=(\d*)-(\d*)/.exec(range); if (m) { if (m[1]) start = Number(m[1]); if (m[2]) end = Number(m[2]); if (!m[1] && m[2]) { start = st.size - Number(m[2]); end = st.size - 1; } if (start > end || end >= st.size) return new Response(null, { status: 416, headers: { "Content-Range": `bytes */${st.size}` } }); status = 206; headers["Content-Range"] = `bytes ${start}-${end}/${st.size}`; } } headers["Content-Length"] = String(end - start + 1); if (head) return new Response(null, { status, headers }); return new Response(Readable.toWeb(createReadStream(path, { start, end })) as unknown as ReadableStream, { status, headers }); }